classdef dotsQueryableEye < dotsQueryable
    % @class dotsQueryableEye
    % Superclass for objects that acquire and classify data from an eye
    % tracker.
    % @details
    % dotsQueryableEye extends the dotsQueryable superclass with support
    % for eyetrackers that measuer x and y point of gaze, and pupil
    % size.
    % @details
    % <b>A note about Eye Tracker Coordinates:</b>
    % @details
    % dotsQueryableEye transforms x and y position data from raw eye
    % tracker coordinates into a user-specified coordinate system that
    % might be more natural, such as degrees of visual angle.  It uses
    % inputRect to impose a coordinate system on the raw data and presents
    % data in a coordinate sytem relative to xyRect.
    % @details
    % Internally, it uses inputRect and xyRect to do linear transforms on
    % the position data.  It transforms raw data out of the inputRect
    % coordinates, then into the xyRect coordinates.  Thus, it maps one
    % coordinate system onto the other.
    % @details
    % inputRect should refer to a rectangular region in the eye tracker's
    % coordinate system, perhaps part of a calibration pattern.  If
    % dotsTheMachineConfiguration contains an 'inputRect' entry for a given
    % dotsQueryableEye subclass, eye tracker-specific coordinates will be
    % automatically transformed away.
    % @details.
    % xyRect should refer to the same physical region as inputRect, but may
    % use any coordinate system.  dotsTheMachineConfiguration might
    % contain an 'xyRect' entry for a given dotsQueryableEye subclass in
    % order to establish a default coordinate system.
    % @details
    % Or, a subclass might use openEyeTracker() to chose a coordinate
    % system at the time its created.  To help with chosing an xyRect,
    % dotsQueryableEye automatically shares properties related to the
    % visual scene, such as windowRect and pixelsPerDegree. Note that these
    % modes might not have reasonable values until a drawing window has
    % been opened.
    % @details
    % inputRect and xyRect should have the form [x y width height].  x and
    % width specify the linear transformation for horizontal data, y and
    % height specify the vertical transform.  width or height may be
    % negative, in order to flip an axis.  The default for each rectangle
    % is [0 0 1 1], specifying the identity transform (no transform).
    % @details
    % <b>Subclasses</b>
    % @details
    % dotsQueryableEye itself is not a usable class, it just provides a
    % uniform interface and core functionality to subclasses (see below).
    % Subclasses must fill in some details:
    %   - openEyeTracker() This is a mandatory method which subclasses must
    %   redefine.  It must locate, connect to, initialize, etc. a suitable
    %   eyetracker device.  It must return true if successful, or else
    %   return false.
    %   - closeEyeTracker() This method must release any devide resources
    %   acquired during openEyeTracker().
    %   - readRawData() This is mandatory method which subclasses must
    %   redefine.  It must acquire point of gaze and pupil size from
    %   the device acquired during openEyeTracker().  It must return a
    %   matrix of data in the 3-column dotsQueryable format.
    %   .
    % @ingroup queryable
    
    properties
        % rectangle to establish eye tracker device coordinates ([x y w h])
        % @details
        % inputRect describes a rectangular region using the eye tracker's
        % coordinate system (xyRect describes the same region using a
        % different coordinate system).  dotsQueryableEye transforms raw
        % position data <b>out of</b> the inputRect coordinates.
        inputRect = [0 0 1 1];
        
        % rectangle to establish user-defined coordinates ([x y w h])
        % @details
        % xyRect describes a rectangular region using an arbitrary
        % coordinate system (inputRect describes the same region using
        % device-specific coordinates).  dotsQueryableEye transforms raw
        % position data <b>into</b> the xyRect coordinates.
        % @details
        % xyRect also defines the region of interest used during querying
        % to classify eye position data.
        xyRect = [0 0 1 1];
        
        % number of points to sample along each dimension of xyRect
        % @details
        % xyPoints is only used during querying to classify eye position
        % data.
        xyPoints = [100 100];
        
        % the current "x" position of the eye, in the coordinates
        % established by xyOffset and xyScale.
        xPos;
        
        % the current "y" position of the eye, in the coordinates
        % established by xyOffset and xyScale.
        yPos;
        
        % maximum pupil size of interest
        % @details
        % pupilMax is only used during querying to classify pupil size
        % data.
        pupilMax = 1;
        
        % number of points to sample along the pupil size dimension
        % @details
        % pupilPoints is only used during querying to classify pupil size
        % data.
        pupilPoints = 10;
        
        % how to scale raw pupil size data
        pupilScale = 1;
        
        % the current pupil size, in units established by pupilScale
        pupil;
        
        % frequency in Hz of eye tracker data samples
        % @details
        % Subclasses must supply the sample frequency of their eye tracker
        % device.  dotsTheMachineConfiguration might help.
        sampleFrequency = 1;
        
        % interger identifier for x-position data
        % @details
        % Subclasses must assign this ID to x-position data during
        % readRawData().  openEyeTracker() may set an xID different from
        % the default.
        xID = 1;
        
        % interger identifier for y-position data
        % @details
        % Subclasses must use this ID for y-position data during
        % readRawData().  openEyeTracker() may set a yID different from
        % the default.
        yID = 2;
        
        % interger identifier for pupil size data
        % @details
        % Subclasses must use this ID for pupil size data during
        % readRawData().  openEyeTracker() may set a pupilID different
        % from the default.
        pupilID = 3;
        
        % Psychtoolbox Screen index of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowNumber;
        
        % pixel dimensions of the current drawing window [x y x2 y2]
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowRect;
        
        % Hz frame rate of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowFrameRate;
        
        % degrees-to-pixels factor for the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        pixelsPerDegree;
    end
    
    properties (Access = public)
        % coefficient for x-position coordinate transform
        xScale;
        
        % coefficient for y-position coordinate transform
        yScale;
        
        % offset for x-position coordinate transform
        xOffset;
        
        % offset for y-position coordinate transform
        yOffset;
    end
    
    methods
        % Constructor takes no arguments.
        % @details
        % User should use dotsTheQueryablesManager to create new objects.
        function self = dotsQueryableEye
            self = self@dotsQueryable;
            
            % screen shared properties
            dotsTheSwitchboard.sharePropertyOfObject('windowNumber', self);
            dotsTheSwitchboard.sharePropertyOfObject('windowRect', self);
            dotsTheSwitchboard.sharePropertyOfObject('windowFrameRate', self);
            dotsTheSwitchboard.sharePropertyOfObject('pixelsPerDegree', self);
        end
        
        % Release resources for this device.
        % @details
        % Matlab calls delete() automatically when it's done using the
        % object.
        function delete(self)
            self.closeEyeTracker();
        end
        
        % Try to acquire an eye tracker device.
        % @details
        % Returns true if openEyeTracker() can locate, initialize, connect
        % to, etc. a suitable eye tracker device.  Otherwise returns false.
        function isAvailable = checkIsAvailable(self)
            isAvailable = self.openEyeTracker;
        end
        
        % Acquire device-specific resources.
        % @details
        % Subclasses need to redefine openEyeTracker() to acquire any
        % resources or connections for an eye tracker device.
        function isOpen = openEyeTracker(self)
            isOpen = true;
        end
        
        % Release device-specific resources.
        % @details
        % Subclasses need to redefine closeEyeTracker() to release any
        % resources or connections acquired during openEyeTracker().
        function closeEyeTracker(self)
        end
        
        % Read raw data from the acquired eye tracker device.
        % @details
        % Subclasses need to redefine readRawData() to aquire the latest
        % data from the eye tracker device acquired during
        % openEyeTracker().  The data must be returned in the dotsQueryable
        % format, using xID, yID, and pupilID as the data dimension IDs.
        % @details
        % The data returned frmo readRawData() should not be transformed
        % yet.
        function data = readRawData(self)
            data = zeros(0,3);
        end
        
        % Acquire the lastest eye tracker data.
        % @details
        % Gets raw data from the eye tracker using the readRawData().
        % Transforms the data with transformRawData().
        function readData(self)
            if self.isAvailable
                self.appendData(self.readRawData);
            end
        end
        
        % Add formatted data to allData and do accounting.
        % @param data nx3 double matrix of formatted data.
        % See the dotsQueryable allData property for the correct data
        % format.
        % @details
        % Extends the dotsQueryable appendData() method to transform x, y,
        % and pupil size into useful coordinates, and copy the most
        % recent value from each dimension into a convenient property.
        function appendData(self, newData)
            newData = self.transformRawData(newData);
            self.appendData@dotsQueryable(newData);
        end
        
        % Clear data from this object.
        % @details
        % Extends the dotsQueryable flushData() method to do also clear the
        % current xPos, yPos, and size values;
        function flushData(self)
            self.flushData@dotsQueryable;
            self.xPos = 0;
            self.yPos = 0;
            self.pupil = 0;
        end
        
        % Show a figure with continuously updated eye data.
        % @details
        % Opens a new figure and plots eye position and pupil size.
        % Continuously updates the plot data as long as the figure is open.
        % Should be a handy way to test or inspect a device.
        % @details
        % Shows eye position values and pupil size in separate
        % subplots.
        function plotData(self)
            if self.isAvailable
                
                f = figure;
                xDim = self.dimensionByID(self.xID);
                yDim = self.dimensionByID(self.yID);
                xTicks = unique([ ...
                    self.dimensions(xDim).minValue, ...
                    self.dimensions(xDim).maxValue, ...
                    self.dimensions(xDim).neutralValue]);
                yTicks = unique([ ...
                    self.dimensions(yDim).minValue, ...
                    self.dimensions(yDim).maxValue, ...
                    self.dimensions(yDim).neutralValue]);
                ax = subplot(10,10,1:80, ...
                    'Parent', f, ...
                    'XLim', [min(xTicks), max(xTicks)+eps], ...
                    'XTick', xTicks, ...
                    'YLim', [min(yTicks), max(yTicks)+eps], ...
                    'YTick', yTicks);
                xlabel(self.dimensions(xDim).name);
                ylabel(self.dimensions(yDim).name);
                xyLine = line(self.xPos, self.yPos, ...
                    'Parent', ax, ...
                    'LineStyle', 'none', ...
                    'Marker', 'o', ...
                    'Color', 'k');
                
                pupilDim = self.dimensionByID(self.pupilID);
                dTicks = unique([ ...
                    self.dimensions(pupilDim).minValue, ...
                    self.dimensions(pupilDim).maxValue, ...
                    self.dimensions(pupilDim).neutralValue]);
                ax = subplot(10,10,91:100, ...
                    'Parent', f, ...
                    'YLim', [0, 1], ...
                    'YTick', [], ...
                    'XLim', [min(dTicks), max(dTicks)+eps], ...
                    'XTick', dTicks);
                xlabel(self.dimensions(pupilDim).name);
                pupilLine = line(self.pupil, .5, ...
                    'Parent', ax, ...
                    'LineStyle', 'none', ...
                    'Marker', '+', 'MarkerSize', 10);
                
                self.flushData;
                waitTime = 1./self.sampleFrequency;
                while ishandle(f)
                    self.readData;
                    output = self.queryAsOfTime;
                    if ~isempty(output)
                        set(xyLine, ...
                            'XData', self.xPos, ...
                            'YData', self.yPos, ...
                            'Color', 'r');
                    else
                        set(xyLine, ...
                            'XData', self.xPos, ...
                            'YData', self.yPos, ...
                            'Color', 'k');
                    end
                    set(pupilLine, 'XData', self.pupil);
                    drawnow;
                    pause(waitTime);
                end
            end
        end
    end
    
    methods (Access = protected)
        % Configre the array of dotsFiniteDimension to represent the space
        % of x, y, and pupil size.
        % @details
        % The coordinate transform out of inputRect and into xyRect
        % coordinates is prepared during setupDimensions().
        function setupDimensions(self)
            self.setupCoordinateRectTransform;
            
            self.dimensions(1) = dotsFiniteDimension.newLinear( ...
                self.xyRect(1), ...
                self.xyRect(1)+self.xyRect(3), ...
                self.xyPoints(1));
            self.dimensions(1).name = 'xPos';
            self.dimensions(1).ID = self.xID;
            self.dimensions(1).neutralValue = self.xyRect(1)+self.xyRect(3)/2;
            % don't track x
            self.isTrackingDimension(1) = false;
            
            self.dimensions(2) = dotsFiniteDimension.newLinear( ...
                self.xyRect(2), ...
                self.xyRect(2)+self.xyRect(4), ...
                self.xyPoints(2));
            self.dimensions(2).name = 'yPos';
            self.dimensions(2).ID = self.yID;
            self.dimensions(2).neutralValue = self.xyRect(2)+self.xyRect(4)/2;
            % don't track y
            self.isTrackingDimension(2) = false;
            
            self.dimensions(3) = dotsFiniteDimension.newLinear( ...
                0, ...
                self.pupilMax, ...
                self.pupilPoints);
            self.dimensions(3).name = 'pupilSize';
            self.dimensions(3).ID = self.pupilID;
            self.dimensions(3).neutralValue = 0;
            % don't track pupil size
            self.isTrackingDimension(3) = false;
        end
        
        % Identify handy, reusable regions in the object's data space.
        % @details
        %   - negative and positive for x and y
        %   - zero pupil size (blinking, sleeping, etc.)
        %   .
        function setupDefaultPhenomenons(self)
            % negative and positive for x and y
            temp = self.getPhenomenonTemplate;
            xyDims = self.dimensionByID([self.xID, self.yID]);
            for xy = xyDims
                d = self.dimensions(xy);
                negative = dotsPhenomenon.partition(temp, ...
                    d.name, 0, '<');
                negative.name = sprintf('negative_%s', d.name);
                self.addPhenomenonToGroup(negative, 'point of gaze');
                
                positive = dotsPhenomenon.partition(temp, ...
                    d.name, 0, '>=');
                positive.name = sprintf('positive_%s', d.name);
                self.addPhenomenonToGroup(positive, 'point of gaze');
            end
            
            % zero for pupil size
            dDim = self.dimensionByID(self.pupilID);
            d = self.dimensions(dDim);
            closed = dotsPhenomenon.partition(temp, ...
                d.name, 0, '<=');
            closed.name = 'eye closed';
            self.addPhenomenonToGroup(closed, 'pupil size');
        end
        
        % Replace x, y, and size data with transformed data.
        % @param newData nx3 double matrix of formatted data, as passed to
        % appendData.
        % @details
        % Finds "x" and "y" position data in @newData and does the
        % transform from inputRect to xyRect coordinates.  Finds pupil
        % size data and scales by pupilScale.  Returns @a newData with the
        % transformed x, y, and pupil size data.
        % @details
        % Updates the xPos, yPos and pupil properties with the latest,
        % transformed values.  Assumes data are sorted by increasing time.
        function newData = transformRawData(self, newData)
            xSelector = newData(:,1) == self.xID;
            if any(xSelector)
                transX = self.xOffset + self.xScale*newData(xSelector,2);
                newData(xSelector,2) = transX;
                self.xPos = transX(end);
            end
            
            ySelector = newData(:,1) == self.yID;
            if any(ySelector)
                transY = self.yOffset + self.yScale*newData(ySelector,2);
                newData(ySelector,2) = transY;
                self.yPos = transY(end);
            end
            
            dSelector = newData(:,1) == self.pupilID;
            if any(dSelector)
                transD = self.pupilScale*newData(dSelector,2);
                newData(dSelector,2) = transD;
                self.pupil = transD(end);
            end
        end
        
        % Prepare the transform from inputRect to xyRect coordinates.
        % @details
        % Combines the transforms out of inputRect coordinates and into
        % xyRect coordinates into a single transform to be applied to data
        % during appendData().
        function setupCoordinateRectTransform(self)
            self.xScale = self.xyRect(3)/self.inputRect(3);
            self.xOffset = self.xyRect(1) - self.inputRect(1)*self.xScale;
            self.yScale = self.xyRect(4)/self.inputRect(4);
            self.yOffset = self.xyRect(2) - self.inputRect(2)*self.yScale;
        end
    end
end